<?php

class modelBase {
	protected $db;
	public $aether;
	protected $m;
	protected $cache;
	protected $stable = false;
	protected $name;
	protected $dbLink;
	protected $cols = array();
	protected $validations;
	protected $SL = false;
	protected $stabilityHandler = false;
	protected $backup = false;
	protected $default = false;
	protected $owned = false;
	protected $oldCols;

	function __construct($args = null) {
		if($args) {
			$args = arrayExtend($args, array(
				'name' => 'noName',
				'dbLink' => true,
				'cols' => array(),
				'validations' => array(),
			));
			$this->name = $args['name'];
			$this->dbLink = $args['dbLink'];
			$this->cols = $args['cols'];
			$this->validations = $args['validations'];
		}
	}

	public function init($aether) {
		//Set reference variables
		$this->aether = $aether;
		$this->db = $aether->db;
		$this->m = $aether->models;
		$this->cache = $this->aether->cache;
		
		//Add table prefix
		if($this->aether->config->get('database', 'prefix')) {
			$this->name = $this->aether->config->get('database', 'prefix') . '_' . $this->name;
		}
		
		//Can't forget ID!
		$this->cols = arrayPrepend($this->cols, array(
			'id' => array(
				'type' => 'INT', 
				'primaryKey' => true, 
				'autoIncrement' => true,
				'auto' => true
			)
		));

		if($this->owned) {
			$this->cols = arrayAppend($this->cols, array(
				'parentId' => array(
					'type' => 'INT',
					'index' => true
				)
			));
		}

		$this->oldCols = $this->cols;
		
		//Process all the columns
		foreach($this->cols as $name => $def) {
			
			if(is_object($def)) {
				$this->cols[$name]->init($this->aether);
			} else if(isset($def['hasMany'])) {
				$this->aether->loader->load($def['hasMany'], 'app', 'model');
				$className = $def['hasMany'];
				$this->cols[$name] = $this->m->$className;
			} else {
				//Otherwise, find out column type and add appropriate default values
				//Set defaults
				$def = arrayExtend($def, array(
					'auto' => false,
					'notNull' => true,
					'autoIncrement' => false,
					'primaryKey' => false,
					'md5' => false,
					'index' => false,
					'default' => false,
					'unique' => false
				));

				if($def['autoIncrement']) {
					$def['unique'] = true;
				}

				if($def['default']) {
					$this->default = $name;
				}
				
				//If its an auto column, add correct arguments
				if($def['auto']) {
					if($name=='parent') {
						$def['type'] = 'INT';
						$def['unsigned'] = true;
					} else if($name=='updatedAt'||$name=='createdAt') {
						$def['type'] = 'TIMESTAMP';
						$def['notNull'] = true;
					}
				}

				if(!isset($def['type'])) {
					die('Aether Error: Must define type.  Model: '.$this->name.'; Column: '.$name);
				}
				
				//If its md5, add correct arguments
				if($def['type']=='MD5') {
					$def['type'] = 'VARCHAR';
					$def['length'] = 32;
					$def['md5'] = true;
				} else if($def['type']=='BOOLEAN') {
					$def['type'] = 'TINYINT';
					$def['length'] = 1;
				} else if($def['type']=='VARCHAR') {
					sins($def['length'], 32);
				}
				
				if($def['type']=='INT') {
					sins($def['unsigned'],true);
				} else {
					sins($def['unsigned'],false);
				}

				$this->cols[$name] = $def;
			}
			
		}
	}

	public function getCols() {
		return $this->cols;
	}

	public function getCol($name) {
		return $this->cols[$name];
	}

	public function getColList() {
		$cols = array();
		reset($this->cols);
		for($i = 0; current($this->cols); $i++) {
			$cols[$i] = key($this->cols);
			next($this->cols);
		}
		return $cols;
	}

	public function getName() {
		return $this->name;
	}
	
	public function setName($name) {
		$this->name = $name;
		foreach($this->cols as $colName => $col) {
			if(is_object($col)) {
				$this->cols[$colName]->setName($name . $colName);
			}
		}
	}

	public function dbLink() {
		return $this->dbLink;
	}

	public function owned() {
		return $this->owned;
	}

	public function getType($name) {
		return $this->cols[$name]['type'];
	}

	public function getAutoIncrementValue($column) {
		$name = $this->getName().'-'.$column;
		if($this->cache->isAvailable($name)) {
			$value = (int) $this->cache->get($name);
		} else {
			$result = $this->get('all', array(
				'results' => 'single',
				'resultsType' => 'array',
				'orderBy' => array($column, 'DESC'),
				'columns' => array($column)
			));
			$value = $result[$column];
		}
		$value++;
		$this->cache->set($name, $value);
		return $value;
	}

	public function insert($data, $args = array()) {
		if(is_array($data)) {
			$arr = array();
			foreach($data as $cur) {
				$return = $this->insert($cur, $args);
				if(!$return) {
					$this->delete($arr); //Undo what has been done
					return false;
				}
				$arr = arrayAppend($arr, array($return));
			}
			return $arr;
		}
		if(!$data->validated()) {
			if(!$this->validate($data)) {
				return false;
			}
		}
		$query = 'INSERT INTO `'.$this->name.'` (';
		$first = true;
		$values = '';
		foreach($this->cols as $name => $column) {
			if((!$data->hasChild($name)||!$data->child($name)->getValue())&&!is_object($column)&&$column['autoIncrement']) {
				$data->setChild($name, new data($this->getAutoIncrementValue($name)));
			}
			if($data->hasChild($name)&&!is_object($column)) {
				if(!$first) {
					$query .= ', ';
					$values .= ', ';
				} else {
					$first = false;
				}
				$values .= $this->serialize($this->cols[$name]['type'], $data->child($name));
				$query .= '`'.$name.'`';
			}
		}
		$query .= ') VALUES ('.$values.')';
		if(!$this->db->query($query)) {
			return false;
		}
		$id = $data->child('id')->getValue();
		foreach($this->cols as $name => $column) {
			if($data->hasChild($name)&&is_object($column)) {
				$list = &$data->child($name)->listChildren();
				foreach($list as $child) {
					$child->child('parentId')->setValue($id);
					$child->deleteChild('id');
					$column->insert($child);
				}
			}
		}

		if($this->stable&&$this->stabilityHandler) {
			$this->stabilityHandler->recordUpdate(SL_INSERT, $this->name, 0, $id);
		}

		return $id;
	}

	/*
	Args:
	columns: array()
	where: conditional
	limit: array(start, length)
	orderBy: array(column(s), 'ASC'/'DESC')
	sql: string
	results: 'raw' or 'array' or 'single'
	resultsType: 'data' or 'array'
	*/

	public function get($id = 'all', $args = array()) {
		$args = arrayExtend($args, array(
			'columns' => 'all',
			'limit' => array(0, 10),
			'where' => null,
			'results' => 'array',
			'resultsType' => 'data'
		));
		if($args['results']=='single') {
			$args['limit'][1] = $args['limit'][0] + 1;
		}
		$sql = 'SELECT ';
		if($args['columns']=='all') {
			$sql .= '*';
		} else if(is_array($args['columns'])) {
			$first = true;
			foreach($args['columns'] as $column) {
				if(!is_object($this->cols[$column])) {
					if($first) {
						$first = false;
					} else {
						$sql .= ', ';
					}
					$sql .= '`'.$column.'`';
				}
			}
		} else {
			$sql .= '`'.$args['columns'].'`';
		}
		$sql .= ' FROM `'.$this->name.'`';

		$where = $this->insertIdIntoWhere($args['where'], $id);
		if($where) {
			$sql .= ' WHERE '.$where->toSQL($this);
		}

		if(isset($args['orderBy'])) {
			$sql .= ' ORDER BY ';
			if(is_array($args['orderBy'][0])) {
				$first = true;
				foreach($args['orderBy'][0] as $column) {
					if($first) {
						$first = false;
					} else {
						$sql .= ', ';
					}
					$sql .= '`'.$column.'`';
				}
			} else {
				$sql .= '`'.$args['orderBy'][0].'`';
			}
			$sql .= ' '.$args['orderBy'][1];
		}

		if($args['limit']) {
			$sql .= ' LIMIT '.$args['limit'][0].', '.$args['limit'][1];
		}

		if(isset($args['sql'])) {
			$sql .= ' '.$args['sql'];
		}
		if(!$results = $this->db->query($sql)) {
			return false;
		}
		if($args['results']=='raw') {
			return $results;
		}
		$return = array();
		if($args['columns']=='all') {
			$args['columns'] = $this->getColList();
		}
		for($i = 0; $row = $this->db->nextAssoc($results); $i++) {
			$return[$i] = array();
			foreach($args['columns'] as $column) {
				if(is_object($this->cols[$column])) {
					$return[$i][$column] = $this->getAssoc($column, $row['id'], $args);
				} else {
					$return[$i][$column] = $row[$column];
				}
			}
			if($args['resultsType']=='data') {
				$return[$i] = new data($return[$i]);
			}
		}
		if($args['results']=='array') {
			return $return;
		} else {
			if(isset($return[0])) {
				return $return[0];
			} else {
				return null;
			}
		}
	}

	public function delete($id, $args = array()) {
		$args = arrayExtend($args, array(
			'where' => null
		));

		//Get what rows are being affected
		$where = $this->insertIdIntoWhere($args['where'], $id);
		$affectedRows = $this->get('all', array(
			'where' => $where
		));

		//Add to stability layer and delete associated columns
		foreach($affectedRows as $row) {
			$origId = $row->child('id')->getValue();
			if($this->stable&&$this->stabilityHandler) {
				$row->deleteChild('id');
				$id = $this->SL()->insert($row);
				$this->stabilityHandler->recordUpdate(SL_DELETE, $this->name, $id, $origId);
			}
			foreach($this->cols as $col) {
				if(is_object($col)) {
					$col->delete('all', array(
						'where' => new predicate('parentId', '=', $origId)
					));
				}
			}
		}

		//Delete the rows
		$sql = 'DELETE FROM `'.$this->name.'`';
		if($where) {
			$sql .= ' WHERE '.$where->toSQL($this);
		}
		return $this->db->query($sql);
	}

	public function insertIdIntoWhere($where, $id) {
		if(is_numeric($id)) {
			$where = constraint::newConstraint(array($where, '&&', new predicate('id', '=', $id)));
		} else if(is_array($id)) {
			$previous = null;
			foreach($id as $sid) {
				$previous = constraint::newConstraint(array($previous, '||', new predicate('id', '=', $sid)));
			}
			$where = constraint::newConstraint(array($where, '&&', $previous));
		}
		return $where;
	}

	public function getAssoc($column, $id, $args) {
		return $this->cols[$column]->get('all', array(
			'where' => new predicate('parentId', '=', $id),
			'limit' => null,
			'resultsType' => $args['resultsType']
		));
	}

	public function update($id, $data, $args = array()) {
		$args = arrayExtend($args, array(
			'where' => null
		));
	
		$where = $this->insertIdIntoWhere($args['where'], $id);
		$affectedRows = $this->get('all', array(
			'where' => $where
		));
		
		$ids = array();
		foreach($affectedRows as $row) {
			if(!isset($whereId)) {
				$whereId = '';
			} else {
				$whereId .= ' || ';
			}
			$whereId .= '`id`='.$row->child('id')->getValue();
			$ids = arrayAppend($ids, array(
				$row->child('id')->getValue()
			));
			if($this->stable&&$this->stabilityHandler) {
				$id = $row->child('id')->getValue();
				$row->deleteChild('id');
				$original = $this->SL()->insert($row);
				$this->stabilityHandler->recordUpdate(SL_UPDATE, $this->name, $original, $id); 
			}
		}

		$sql = 'UPDATE `'.$this->name.'` SET ';
		$data->deleteChild('id');
		$children = $data->listChildren();
		$first = true;
		foreach($children as $child) {
			$name = $child->getName();
			if(isset($this->cols[$name])) {
				if(!is_object($this->cols[$name])) {
					if($first) {
						$first = false;
					} else {
						$sql .= ', ';
					}
					$sql .= '`'.$name.'` = '.$this->serialize($this->cols[$name]['type'], $child);
				} else {
					foreach($ids as $id) {
						$this->cols[$name]->delete('all', array(
							'where' => new predicate('parentId', '=', $id)
						));
						$grandchildren = $child->listChildren();
						foreach($grandchildren as $grandchild) {
							$grandchild->addChild($id, 'parentId');
						}
						$this->cols[$name]->insert($grandchildren);
					}
				}
			}
		}
		$sql .= ' WHERE '.$whereId;
		return $this->db->query($sql);
	}

	public function serialize($type, $data) {
		$value = $data->getValue();
		if($type=='VARCHAR'||$type=='CHAR'||$type=='BINARY'||$type=='VARBINARY'||$type=='BLOB'||$type=='TEXT'||$type=='ENUM'||$type=='SET') {
			return '"'.mysql_real_escape_string($value).'"';
		} else if($type=='INT'||$type=='TINYINT'||$type=='MEDIUMINT'||$type=='BIGINT'||$type=='INTEGER') {
			return intval($value);
		} else if($type=='DECIMAL'||$type=='DEC'||$type=='FLOAT') {
			return floatval($value);
		} else if($type=='TIMESTAMP') {
			if(is_numeric($value)) {
				return 'FROM_UNIXTIME('.$value.')';
			} else {
				return '"'.mysql_real_escape_string($value).'"';
			}
		}
	}

	/*
	Model functions:
	* Insert
	* Select
	* Update
	* Delete
	* Search?
	* Private Where?

	Stability Layer functions:
	
	*/

	public function setStabilityHandler($handler) {
		$this->stabilityHandler = $handler;
	}

	public function initSL() {
		$this->SL = clone $this;
		$this->SL->setName($this->getName().'SL');
		$this->SL->setStable(false);
	}

	function __clone() {
		foreach($this->cols as $name => $col) {
			if(is_object($col)) {
				$this->cols[$name] = clone $this->cols[$name];
			}
		}
	}

	public function setStable($stable) {
		$this->stable = $stable;
	}

	public function isStable() {
		return $this->stable;
	}
	
	public function SL() {
		if($this->stable) {
			if(!$this->SL) {
				$this->initSL();
			}
			return $this->SL;
		} else {
			return false;
		}
	}

	protected function createChildren($data) {
		$data->child($this->default)->setValue($data->getValue());
		return $data;
	}

	public function validate($data) {
		if(!is_object($data)) {
			$this->createChildren($data);
		}
		$errorless = true;
		foreach($this->cols as $name => $column) {
			if(isset($this->validations[$name])) {
				$validations = $this->validations[$name];
			} else {
				$validations = array();
			}
			if(!$data->hasChild($name)) {
				$data->addChild(array(), $name);
			}
			if(is_object($column)) {
				if(isset($this->validations[$name]['validations'])) {
					$validations = $this->validations[$name]['validations'];
				} else {
					$validations = array();
				}
				if($data->child($name)->hasChildren()) {
					$list = $data->child($name)->listChildren();
					foreach($list as $child) {
						if(!$this->cols[$name]->validate($child)) {
							$errorless = false;
						}
					}
				}
			}
			foreach($validations as $validation) {
				$method = $validation[0];
				if(method_exists($this->aether->helpers->validate, $method)) {
					$validator = $this->aether->helpers->validate;
				} else if(method_exists($this->aether->helpers->coreValidate, $method)) {
					$validator = $this->aether->helpers->coreValidate;
				} else {
					die('Validator method '.$method.' does not exist.');
				}
				$child = $data->child($name);
				if(!$validator->$method($child, $validation, array(
					'model' => $this,
					'column' => $this
				))) {
					$errorless = false;
					$data->child($name)->addError($validation['message']);
				}
			}
		}
		if($errorless) {
			$data->setValidated(true);
		}
		return $errorless;

	}
}

//EOF
